Kuasai kombinator Promise JavaScript (Promise.all, Promise.allSettled, Promise.race, Promise.any) untuk pemrograman asinkron yang efisien dan tangguh dalam aplikasi global.
Kombinator Promise JavaScript: Pola Asinkron Lanjutan untuk Aplikasi Global
Pemrograman asinkron adalah landasan JavaScript modern, terutama saat membangun aplikasi web yang berinteraksi dengan API, basis data, atau melakukan operasi yang memakan waktu. Promise JavaScript menyediakan abstraksi yang kuat untuk mengelola operasi asinkron, tetapi menguasainya memerlukan pemahaman pola-pola lanjutan. Artikel ini akan membahas secara mendalam tentang kombinator Promise JavaScript – Promise.all, Promise.allSettled, Promise.race, dan Promise.any – dan bagaimana mereka dapat digunakan untuk menciptakan alur kerja asinkron yang efisien dan tangguh, terutama dalam konteks aplikasi global dengan kondisi jaringan dan sumber data yang bervariasi.
Memahami Promise: Rekap Singkat
Sebelum mendalami kombinator, mari kita tinjau kembali Promise secara singkat. Sebuah Promise mewakili hasil akhir dari sebuah operasi asinkron. Promise dapat berada dalam salah satu dari tiga keadaan:
- Pending: Keadaan awal, belum terpenuhi (fulfilled) atau ditolak (rejected).
- Fulfilled: Operasi selesai dengan sukses, dengan nilai hasil.
- Rejected: Operasi gagal, dengan alasan (biasanya objek Error).
Promise menawarkan cara yang lebih bersih dan lebih mudah dikelola untuk menangani operasi asinkron dibandingkan dengan callback tradisional. Promise meningkatkan keterbacaan kode dan menyederhanakan penanganan error. Yang terpenting, mereka juga menjadi dasar bagi kombinator Promise yang akan kita jelajahi.
Kombinator Promise: Mengatur Operasi Asinkron
Kombinator Promise adalah metode statis pada objek Promise yang memungkinkan Anda untuk mengelola dan mengoordinasikan beberapa Promise. Mereka menyediakan alat yang kuat untuk membangun alur kerja asinkron yang kompleks. Mari kita periksa masing-masing secara detail.
Promise.all(): Menjalankan Promise secara Paralel dan Menggabungkan Hasil
Promise.all() menerima sebuah iterable (biasanya sebuah array) dari Promise sebagai masukan dan mengembalikan sebuah Promise tunggal. Promise yang dikembalikan ini akan terpenuhi (fulfill) ketika semua Promise masukan telah terpenuhi. Jika ada salah satu Promise masukan yang ditolak (reject), Promise yang dikembalikan akan segera ditolak dengan alasan dari Promise pertama yang ditolak.
Kasus Penggunaan: Ketika Anda perlu mengambil data dari beberapa API secara bersamaan dan memproses hasil gabungannya, Promise.all() adalah pilihan ideal. Sebagai contoh, bayangkan membangun dasbor yang menampilkan informasi cuaca dari berbagai kota di seluruh dunia. Data setiap kota dapat diambil melalui panggilan API terpisah.
async function fetchWeatherData(city) {
try {
const response = await fetch(`https://api.example.com/weather?city=${city}`); // Ganti dengan endpoint API sungguhan
if (!response.ok) {
throw new Error(`Gagal mengambil data cuaca untuk ${city}`);
}
return await response.json();
} catch (error) {
console.error(`Error mengambil data cuaca untuk ${city}: ${error}`);
throw error; // Lemparkan kembali error agar ditangkap oleh Promise.all
}
}
async function displayWeatherData() {
const cities = ['London', 'Tokyo', 'New York', 'Sydney'];
try {
const weatherDataPromises = cities.map(city => fetchWeatherData(city));
const weatherData = await Promise.all(weatherDataPromises);
weatherData.forEach((data, index) => {
console.log(`Cuaca di ${cities[index]}:`, data);
// Perbarui UI dengan data cuaca
});
} catch (error) {
console.error('Gagal mengambil data cuaca untuk semua kota:', error);
// Tampilkan pesan error kepada pengguna
}
}
displayWeatherData();
Pertimbangan untuk Aplikasi Global:
- Latensi Jaringan: Permintaan ke API yang berbeda di lokasi geografis yang berbeda mungkin mengalami latensi yang bervariasi.
Promise.all()tidak menjamin urutan Promise terpenuhi, hanya bahwa semuanya terpenuhi (atau salah satunya ditolak) sebelum Promise gabungan diselesaikan (settle). - Pembatasan Laju API (Rate Limiting): Jika Anda membuat beberapa permintaan ke API yang sama atau beberapa API dengan batas laju bersama, Anda bisa melebihi batas tersebut. Terapkan strategi seperti antrean permintaan atau menggunakan exponential backoff untuk menangani pembatasan laju dengan baik.
- Penanganan Error: Ingatlah bahwa jika salah satu Promise ditolak, seluruh operasi
Promise.all()akan gagal. Ini mungkin tidak diinginkan jika Anda ingin menampilkan data parsial meskipun beberapa permintaan gagal. Pertimbangkan untuk menggunakanPromise.allSettled()dalam kasus seperti itu (dijelaskan di bawah).
Promise.allSettled(): Menangani Keberhasilan dan Kegagalan secara Individual
Promise.allSettled() mirip dengan Promise.all(), tetapi dengan perbedaan penting: ia menunggu semua Promise masukan untuk diselesaikan (settle), terlepas dari apakah mereka terpenuhi atau ditolak. Promise yang dikembalikan selalu terpenuhi dengan sebuah array objek, masing-masing menjelaskan hasil dari Promise masukan yang sesuai. Setiap objek memiliki properti status (baik "fulfilled" atau "rejected") dan properti value (jika terpenuhi) atau reason (jika ditolak).
Kasus Penggunaan: Ketika Anda perlu mengumpulkan hasil dari beberapa operasi asinkron, dan dapat diterima jika beberapa di antaranya gagal tanpa menyebabkan seluruh operasi gagal, Promise.allSettled() adalah pilihan yang lebih baik. Bayangkan sebuah sistem yang memproses pembayaran melalui beberapa gateway pembayaran. Anda mungkin ingin mencoba semua pembayaran dan mencatat mana yang berhasil dan mana yang gagal.
async function processPayment(paymentGateway, amount) {
try {
const response = await paymentGateway.process(amount); // Ganti dengan integrasi gateway pembayaran sungguhan
if (response.status === 'success') {
return { status: 'fulfilled', value: `Pembayaran berhasil diproses melalui ${paymentGateway.name}` };
} else {
throw new Error(`Pembayaran gagal melalui ${paymentGateway.name}: ${response.message}`);
}
} catch (error) {
return { status: 'rejected', reason: `Pembayaran gagal melalui ${paymentGateway.name}: ${error.message}` };
}
}
async function processMultiplePayments(paymentGateways, amount) {
const paymentPromises = paymentGateways.map(gateway => processPayment(gateway, amount));
const results = await Promise.allSettled(paymentPromises);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(result.value);
} else {
console.error(result.reason);
}
});
// Analisis hasil untuk menentukan keberhasilan/kegagalan secara keseluruhan
const successfulPayments = results.filter(result => result.status === 'fulfilled').length;
const failedPayments = results.filter(result => result.status === 'rejected').length;
console.log(`Pembayaran berhasil: ${successfulPayments}`);
console.log(`Pembayaran gagal: ${failedPayments}`);
}
// Contoh gateway pembayaran
const paymentGateways = [
{ name: 'PayPal', process: (amount) => Promise.resolve({ status: 'success', message: 'Payment successful' }) },
{ name: 'Stripe', process: (amount) => Promise.reject({ status: 'error', message: 'Insufficient funds' }) },
{ name: 'Worldpay', process: (amount) => Promise.resolve({ status: 'success', message: 'Payment successful' }) },
];
processMultiplePayments(paymentGateways, 100);
Pertimbangan untuk Aplikasi Global:
- Ketangguhan:
Promise.allSettled()meningkatkan ketangguhan aplikasi Anda dengan memastikan bahwa semua operasi asinkron dicoba, bahkan jika beberapa di antaranya gagal. Ini sangat penting dalam sistem terdistribusi di mana kegagalan biasa terjadi. - Pelaporan Rinci: Array hasil menyediakan informasi rinci tentang hasil setiap operasi, memungkinkan Anda untuk mencatat error, mencoba kembali operasi yang gagal, atau memberikan umpan balik spesifik kepada pengguna.
- Keberhasilan Parsial: Anda dapat dengan mudah menentukan tingkat keberhasilan keseluruhan dan mengambil tindakan yang sesuai berdasarkan jumlah operasi yang berhasil dan gagal. Misalnya, Anda mungkin menawarkan metode pembayaran alternatif jika gateway utama gagal.
Promise.race(): Memilih Hasil Tercepat
Promise.race() juga menerima sebuah iterable dari Promise sebagai masukan dan mengembalikan sebuah Promise tunggal. Namun, tidak seperti Promise.all() dan Promise.allSettled(), Promise.race() diselesaikan (settle) segera setelah salah satu dari Promise masukan diselesaikan (baik terpenuhi atau ditolak). Promise yang dikembalikan akan terpenuhi atau ditolak dengan nilai atau alasan dari Promise pertama yang diselesaikan.
Kasus Penggunaan: Ketika Anda perlu memilih respons tercepat dari beberapa sumber, Promise.race() adalah pilihan yang baik. Bayangkan menanyakan beberapa server untuk data yang sama dan menggunakan respons pertama yang Anda terima. Ini dapat meningkatkan kinerja dan responsivitas, terutama dalam situasi di mana beberapa server mungkin sementara tidak tersedia atau lebih lambat dari yang lain.
async function fetchDataFromServer(serverURL) {
try {
const response = await fetch(serverURL, {signal: AbortSignal.timeout(5000)}); //Tambahkan timeout 5 detik
if (!response.ok) {
throw new Error(`Gagal mengambil data dari ${serverURL}`);
}
return await response.json();
} catch (error) {
console.error(`Error mengambil data dari ${serverURL}: ${error}`);
throw error;
}
}
async function getFastestResponse() {
const serverURLs = [
'https://server1.example.com/data', // Ganti dengan URL server sungguhan
'https://server2.example.com/data',
'https://server3.example.com/data',
];
try {
const dataPromises = serverURLs.map(serverURL => fetchDataFromServer(serverURL));
const fastestData = await Promise.race(dataPromises);
console.log('Data tercepat diterima:', fastestData);
// Gunakan data tercepat
} catch (error) {
console.error('Gagal mendapatkan data dari server manapun:', error);
// Tangani error
}
}
getFastestResponse();
Pertimbangan untuk Aplikasi Global:
- Timeout: Sangat penting untuk menerapkan timeout saat menggunakan
Promise.race()untuk mencegah Promise yang dikembalikan menunggu tanpa batas waktu jika beberapa Promise masukan tidak pernah diselesaikan. Contoh di atas menggunakan `AbortSignal.timeout()` untuk mencapai ini. - Kondisi Jaringan: Server tercepat mungkin bervariasi tergantung pada lokasi geografis pengguna dan kondisi jaringan. Pertimbangkan untuk menggunakan Content Delivery Network (CDN) untuk mendistribusikan konten Anda dan meningkatkan kinerja bagi pengguna di seluruh dunia.
- Penanganan Error: Jika Promise yang 'memenangkan' perlombaan ditolak, maka seluruh Promise.race akan ditolak. Pastikan setiap Promise memiliki penanganan error yang sesuai untuk mencegah penolakan yang tidak terduga. Juga, jika Promise yang "menang" ditolak karena timeout (seperti yang ditunjukkan di atas), promise lainnya akan terus berjalan di latar belakang. Anda mungkin perlu menambahkan logika untuk membatalkan promise lain tersebut menggunakan `AbortController` jika tidak lagi diperlukan.
Promise.any(): Menerima Pemenuhan Pertama
Promise.any() mirip dengan Promise.race(), tetapi dengan perilaku yang sedikit berbeda. Ia menunggu Promise masukan pertama yang terpenuhi (fulfill). Jika semua Promise masukan ditolak, Promise.any() akan ditolak dengan sebuah AggregateError yang berisi array alasan penolakan.
Kasus Penggunaan: Ketika Anda perlu mengambil data dari beberapa sumber, dan Anda hanya peduli dengan hasil sukses pertama, Promise.any() adalah pilihan yang baik. Ini berguna ketika Anda memiliki sumber data redundan atau API alternatif yang menyediakan informasi yang sama. Ini memprioritaskan keberhasilan di atas kecepatan, karena ia menunggu pemenuhan pertama, bahkan jika beberapa Promise ditolak dengan cepat.
async function fetchDataFromSource(sourceURL) {
try {
const response = await fetch(sourceURL);
if (!response.ok) {
throw new Error(`Gagal mengambil data dari ${sourceURL}`);
}
return await response.json();
} catch (error) {
console.error(`Error mengambil data dari ${sourceURL}: ${error}`);
throw error;
}
}
async function getFirstSuccessfulData() {
const dataSources = [
'https://source1.example.com/data', // Ganti dengan URL sumber data sungguhan
'https://source2.example.com/data',
'https://source3.example.com/data',
];
try {
const dataPromises = dataSources.map(sourceURL => fetchDataFromSource(sourceURL));
const data = await Promise.any(dataPromises);
console.log('Data sukses pertama diterima:', data);
// Gunakan data yang berhasil
} catch (error) {
if (error instanceof AggregateError) {
console.error('Gagal mendapatkan data dari sumber manapun:', error.errors);
// Tangani error
} else {
console.error('Terjadi error yang tidak terduga:', error);
}
}
}
getFirstSuccessfulData();
Pertimbangan untuk Aplikasi Global:
- Redundansi:
Promise.any()sangat berguna ketika berhadapan dengan sumber data redundan yang menyediakan informasi serupa. Jika satu sumber tidak tersedia atau lambat, Anda dapat mengandalkan yang lain untuk menyediakan data. - Penanganan Error: Pastikan untuk menangani
AggregateErroryang dilemparkan ketika semua Promise masukan ditolak. Error ini berisi array alasan penolakan individual, memungkinkan Anda untuk melakukan debug dan mendiagnosis masalah. - Prioritas: Urutan Anda memberikan Promise ke
Promise.any()penting. Tempatkan sumber data yang paling andal atau tercepat di urutan pertama untuk meningkatkan kemungkinan hasil yang sukses.
Memilih Kombinator yang Tepat: Ringkasan
Berikut adalah ringkasan singkat untuk membantu Anda memilih kombinator Promise yang sesuai untuk kebutuhan Anda:
- Promise.all(): Gunakan ketika Anda membutuhkan semua Promise untuk berhasil terpenuhi, dan Anda ingin segera gagal jika ada Promise yang ditolak.
- Promise.allSettled(): Gunakan ketika Anda ingin menunggu semua Promise diselesaikan, terlepas dari keberhasilan atau kegagalan, dan Anda memerlukan informasi rinci tentang setiap hasil.
- Promise.race(): Gunakan ketika Anda ingin memilih hasil tercepat dari beberapa Promise, dan Anda hanya peduli pada yang pertama diselesaikan.
- Promise.any(): Gunakan ketika Anda ingin menerima hasil sukses pertama dari beberapa Promise, dan Anda tidak keberatan jika beberapa Promise ditolak.
Pola Lanjutan dan Praktik Terbaik
Selain penggunaan dasar kombinator Promise, ada beberapa pola lanjutan dan praktik terbaik yang perlu diingat:
Membatasi Konkurensi
Ketika berhadapan dengan sejumlah besar Promise, menjalankannya semua secara paralel mungkin akan membebani sistem Anda atau melebihi batas laju API. Anda dapat membatasi konkurensi menggunakan teknik seperti:
- Chunking: Membagi Promise menjadi kelompok-kelompok kecil (chunk) dan memproses setiap kelompok secara berurutan.
- Menggunakan Semaphore: Menerapkan semaphore untuk mengontrol jumlah operasi yang berjalan bersamaan.
Berikut adalah contoh menggunakan chunking:
async function processInChunks(promises, chunkSize) {
const results = [];
for (let i = 0; i < promises.length; i += chunkSize) {
const chunk = promises.slice(i, i + chunkSize);
const chunkResults = await Promise.all(chunk);
results.push(...chunkResults);
}
return results;
}
// Contoh penggunaan
const myPromises = [...Array(100)].map((_, i) => Promise.resolve(i)); //Buat 100 promise
processInChunks(myPromises, 10) // Proses 10 promise sekaligus
.then(results => console.log('Semua promise terselesaikan:', results));
Menangani Error dengan Baik
Penanganan error yang tepat sangat penting saat bekerja dengan Promise. Gunakan blok try...catch untuk menangkap error yang mungkin terjadi selama operasi asinkron. Pertimbangkan untuk menggunakan pustaka seperti p-retry atau retry untuk mencoba kembali operasi yang gagal secara otomatis.
async function fetchDataWithRetry(url, retries = 3) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (retries > 0) {
console.log(`Mencoba lagi dalam 1 detik... (Sisa percobaan: ${retries})`);
await new Promise(resolve => setTimeout(resolve, 1000)); // Tunggu 1 detik
return fetchDataWithRetry(url, retries - 1);
} else {
console.error('Percobaan maksimal tercapai. Operasi gagal.');
throw error;
}
}
}
Menggunakan Async/Await
async dan await menyediakan cara yang terlihat lebih sinkron untuk bekerja dengan Promise. Mereka dapat secara signifikan meningkatkan keterbacaan dan pemeliharaan kode.
Ingatlah untuk menggunakan blok try...catch di sekitar ekspresi await untuk menangani potensi error.
Pembatalan
Dalam beberapa skenario, Anda mungkin perlu membatalkan Promise yang sedang berjalan, terutama ketika berhadapan dengan operasi yang berjalan lama atau tindakan yang dipicu oleh pengguna. Anda dapat menggunakan API AbortController untuk memberi sinyal bahwa sebuah Promise harus dibatalkan.
const controller = new AbortController();
const signal = controller.signal;
async function fetchDataWithCancellation(url) {
try {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch dibatalkan');
} else {
console.error('Error mengambil data:', error);
}
throw error;
}
}
fetchDataWithCancellation('https://api.example.com/data')
.then(data => console.log('Data diterima:', data))
.catch(error => console.error('Fetch gagal:', error));
// Batalkan operasi fetch setelah 5 detik
setTimeout(() => {
controller.abort();
}, 5000);
Kesimpulan
Kombinator Promise JavaScript adalah alat yang kuat untuk membangun aplikasi asinkron yang tangguh dan efisien. Dengan memahami nuansa dari Promise.all, Promise.allSettled, Promise.race, dan Promise.any, Anda dapat mengatur alur kerja asinkron yang kompleks, menangani error dengan baik, dan mengoptimalkan kinerja. Saat mengembangkan aplikasi global, mempertimbangkan latensi jaringan, batas laju API, dan keandalan sumber data sangatlah penting. Dengan menerapkan pola dan praktik terbaik yang dibahas dalam artikel ini, Anda dapat membuat aplikasi JavaScript yang berkinerja dan tangguh, memberikan pengalaman pengguna yang unggul kepada pengguna di seluruh dunia.